namespace ActiveRecord

import System
import System.Collections.Generic
import System.Data
import System.Diagnostics
import System.Reflection
import System.Text
import System.Text.RegularExpressions
import Inflector.Net

partial class ActiveRecordBase:
   // public methods
   def constructor():
      #debug "${self.GetType().FullName}.ctor"
      if self.GetType() != typeof(ActiveRecordBase):
         init()

   def constructor(*attrs as (string)): 
      _columns = stringAryToHash(*attrs)
      if self.GetType() != typeof(ActiveRecordBase):
         init()

   def constructor(ar as ActiveRecordBase):
      _columns = ar._columns.Clone()
      init()

   public def DecrementCounter(counterName as string) as int:
      return updateCounter(counterName, -1)
   
   // crud methods
   public def Delete() as int:
      sql as string
      if HasValueAtColumn(PrimaryKey):
         sql = "delete from ${TableName} where ${PrimaryKey} = ${_columns[PrimaryKey]}"
      
      iRet as int
      if not Utils.IsEmpty(sql):
         iRet = ActiveRecordBase.NonQuery(sql)
         if iRet == 1:
            updateOwnerCounters(-1)
         else:
            raise InvalidOperationException("Didn't delete any records from ${TableName}.  sql was ${sql}")
         
         return iRet
      else:
         return 0

   public def Find(*conditions as (Expression)) as ActiveRecordBase:
      return ActiveRecordBase.Find(self.GetType(), *conditions)

   public def FindById(id) as ActiveRecordBase:
      return ActiveRecordBase.Find(self.GetType(), SQL.Where(self.PrimaryKey, id))

   public def FindAll(*conditions as (Expression)) as ActiveTable:
      return ActiveRecordBase.FindAll(self.GetType(), *conditions)

   ///<summary>Get associated table through some of association.  If the current active record is lazy, this will cause the associated records to be loaded.</summary>
   public def GetAssociated(table as string) as ActiveTable:
      return GetAssociated(table, false)
   ///<summary>Get associated table through some of association.  If the current active record is lazy, this will cause the associated records to be loaded.</summary>
   public def GetAssociated(table as string, forceReload as bool) as ActiveTable:
      debug "GetAssociated(${table}) - _taintedPK: ${_taintedPK}, _taintedFKs: ${join(_taintedFKs, ',')}, _associations.ContainsKey(table): ${_associations.ContainsKey(table)}"
      
      # if nothing to do, return
      if not forceReload and not _taintedPK and _taintedFKs.Count == 0 and _associations.ContainsKey(table):
         return _associations[table]
      
      load(self, table)
      return _associations[table] if _associations.ContainsKey(table) 
      return null
      
   ///<summary>Returns true if this record has columnName assigned or if any of its database columns have are named columnName</summary>
   public def HasColumn(columnName as string) as bool:
      typeName = self.GetType().AssemblyQualifiedName
      #debug "HasColumn(): checking if ${columnName} exists in: " + join(self._columns.Keys, ',')
      #debug "  and in ${join(_activeRecordTypeColumns[typeName].Keys, ',')}" if _activeRecordTypeColumns.ContainsKey(typeName)
      return not Utils.IsEmpty(columnName) and (self._columns.ContainsKey(columnName) or 
         (_activeRecordTypeColumns.ContainsKey(typeName) and 
            Array.IndexOf(_activeRecordTypeColumns[typeName], columnName) != -1))
            
   
   public def HasValueAtColumn(columnName as string) as bool:
      return HasColumn(columnName) and not Utils.IsEmpty(_columns[columnName])

   public def IncrementCounter(counterName as string) as int:
      return updateCounter(counterName, 1)

   public def Load():
      load(self)

   public def Save() as int:
      return ActiveRecordBase.Save(self)

   // other public methods
   self[index as string] as object:
      get: 
         return _columns[index]
      set:  //FIXME: keep track of tainted values
         #debug "set_Item(${index})"
         if index == PrimaryKey and value != _columns[PrimaryKey]:
            _taintedPK = true
         else: 
            taintedFKCount = _taintedFKs.Count

            for assnList as Dictionary [of string, AssociationAttribute] in (HasOne, HasMany, HasAndBelongsToMany, BelongsTo):
               #debug "set_Item(${index}): assnList.Count: ${assnList.Count}.  Keys: ${join(assnList.Keys, ',')}"

               for tableName in assnList.Keys:
                  fkName = getFKName(tableName, assnList[tableName])
                  
                  if fkName == index and value != _columns[index]:
                     if not _taintedFKs.ContainsKey(tableName):
                        _taintedFKs.Add(tableName, fkName)
                     else:
                        _taintedFKs[tableName] = fkName
                     if not _taintedFKAssns.ContainsKey(fkName):
                        _taintedFKAssns.Add(fkName, assnList[tableName])
                     else:
                        _taintedFKAssns[fkName] = assnList[tableName]
                  
                  #debug "set_Item(${index}): fkName - ${fkName}, tableName: ${tableName}, _taintedFKs: ${join(_taintedFKs, ',')}"
                  
            if _taintedFKs.Count != taintedFKCount: # no fk values were updated, must be content column
               _taintedContent = true

         _columns[index] = value

   public def ReLoad():
      _taintedPK = true
      load(self)

   
   public override def ToString() as string:
      return self.ToString(array(string, 0))

   public def ToString(*keysToExclude) as string:
      sbRet = StringBuilder()
      for item in self._columns:
         # if not in list of keys to exclude and is in list of possible columns
         if (Array.IndexOf(keysToExclude, item.Key) == -1) and isPossibleColumn(item.Key):
            sbRet.Append(item.Key + " = '" + item.Value.ToString().Replace("'", "''") + "', ")
      sbRet.Length = sbRet.Length - 2 if sbRet.Length > 0 # strip off final comma
      return sbRet.ToString()

   // protected / private methods
   private def getFKName(table as string, assn as AssociationAttribute) as string:
      #debug "getFKName(${table}) assn.AssociationType: ${assn.AssociationType}, self.TableName: ${self.TableName}"
      if assn.AssociationType == AssociationType.BelongsTo:
         if not BelongsTo.ContainsKey(table) and not Utils.IsEmpty(TableName):
            return "${Inflector.Singularize(TableName)}_${DEFAULT_IDENTITY_KEY}" 
         else: // found in BelongsTo
            
            belongsTo = BelongsTo[table]
            #debug "belongsTo.ForeignKey: ${belongsTo.ForeignKey}"
            #debug "getFkName(${table}) belongsTo: " + Utils.Inspect(belongsTo)
            return belongsTo.ForeignKey if not Utils.IsEmpty(belongsTo.ForeignKey)
            
            fkName = "${Inflector.Singularize(table)}_${DEFAULT_IDENTITY_KEY}"
            #debug "getFkName(${table}) fkName: " + fkName
            #debug "_activeRecordTypes: ${join(_activeRecordTypes.Keys, ',')}"
            #debug "${TableName}._columns = ${join(_activeRecordTypeColumns[TableName], ',')}"
            if Array.IndexOf(_activeRecordTypeColumns[self.GetType().AssemblyQualifiedName], fkName) != -1:
               return fkName
            else:
               return DEFAULT_IDENTITY_KEY
      elif assn.AssociationType == AssociationType.HasOne or assn.AssociationType == AssociationType.HasMany:
         for assnList as Dictionary [of string, AssociationAttribute] in(HasOne, HasMany):
            #debug "assnList.Count: ${assnList.Count}.  Keys: ${join(assnList.Keys, ',')}"
            if not assnList is null and assnList.ContainsKey(table):
               return assnList[table].AssociationForeignKey if not Utils.IsEmpty(assnList[table].AssociationForeignKey)
            
         return "${Inflector.Singularize(TableName)}_${DEFAULT_IDENTITY_KEY}" if not Utils.IsEmpty(TableName)
      elif assn.AssociationType == AssociationType.HasAndBelongsToMany:
         if HasAndBelongsToMany.ContainsKey(table):
            if not Utils.IsEmpty( HasAndBelongsToMany[table].AssociationForeignKey ):
               return HasAndBelongsToMany[table].AssociationForeignKey
         return "${Inflector.Singularize(TableName)}_${DEFAULT_IDENTITY_KEY}" if not Utils.IsEmpty(TableName)
      
      return DEFAULT_IDENTITY_KEY

   private def hasAssociation(tableName as string) as AssociationAttribute:
      #debug "${self.GetType()}.hasAssociation(${tableName})"
      for assnList as Dictionary [of string, AssociationAttribute] in (HasOne, HasMany, HasAndBelongsToMany, BelongsTo):
      #for assnList in (HasOne, HasMany, HasAndBelongsToMany, BelongsTo):
         if assnList is null:
            #debug " assnList is null"
            continue
         #else:
         #   debug " assnList.GetType(): ${assnList}, count: ${assnList.Count}"
         
         #for assn as AssociationAttribute in assnList.Values:
         for table as string in assnList.Keys:
         #for idx as int in range(assnList.Keys.Count):
         #while assnList.Keys.MoveNext():
            assn = assnList[table]
            #debug " checking association: ${assn}"
            
            if table == tableName:
               return assn
            elif not Utils.IsEmpty(assn.ClassName):
               #debug "  checking assn.ClassName: ${assn.ClassName}"
               classTable = _activeRecordTableNames[ _activeRecordTypeNamesShort[assn.ClassName] ] if _activeRecordTypeNamesShort.ContainsKey(assn.ClassName) and _activeRecordTableNames.ContainsKey(_activeRecordTypeNamesShort[assn.ClassName])
               if classTable == tableName:
                  return assn
         
      debug " didn't find association"
      return null

   private def init():
      
      registerActiveRecord(self)
      
      # set member defaults here when no ActiveRecord custom attribute is given
      self.PrimaryKey = DEFAULT_IDENTITY_KEY 
//      self._associationInheritancePK = DEFAULT_IDENTITY_KEY
      
      # set all members here present in ActiveRecord custom attribute
      typeName = self.GetType().AssemblyQualifiedName
      if _activeRecordAttributes.Count > 0 and _activeRecordAttributes.ContainsKey(typeName):
         activeRecordAttribute = _activeRecordAttributes[typeName].ActiveRecordAttribute
         #debug Utils.Inspect(activeRecordAttribute)
         if not activeRecordAttribute is null: 
            if not Utils.IsEmpty(activeRecordAttribute.PrimaryKey):
               #debug "_activeRecordAttribute.PrimaryKey: ${_activeRecordAttribute.PrimaryKey.GetType()} ${_activeRecordAttribute.PrimaryKey}"
               self.PrimaryKey = activeRecordAttribute.PrimaryKey
      
      if not isRegistered(typeName):
         return

   private def isPossibleColumn(key as string) as bool:
      return isPossibleColumn(self.GetType().AssemblyQualifiedName, key)
   
   private def loadAssociation(table as string, forceReloadAssociation as bool):
      if forceReloadAssociation or not _associations.ContainsKey(table): 
         assn = hasAssociation(table)

         if assn is null:
            return
         
         where as WhereExpression
         tableName as string
         
         where,tableName = getAssociatedWhereTableName(assn, self, table)
         
         if where == null or tableName == null:
            return
         
         if _associations.ContainsKey(table):
            _associations[table] = FindBySql(getSelectCmdFromConditions(tableName, where), self.GetType())
         else:
            _associations.Add(table, FindBySql(getSelectCmdFromConditions(tableName, where), self.GetType()))
      
   private def updateCounter(counterName as string, count as int) as int:
      return updateCounter(self.TableName, counterName, self[self.PrimaryKey], self.PrimaryKey, count)
   
   private def updateOwnerCounters(count as int):
      for attList as Dictionary [of string, AssociationAttribute] in (HasAndBelongsToMany, BelongsTo):
         for tableName in attList.Keys:
            counterCacheColumn as string
            att = attList[tableName];
            typeName = getAssociatedTypeNameFromTable(tableName, att)
            #debug "updateOwnerCounters() - update counter cache: tableName: ${tableName}, typeName: ${typeName}"
            if Utils.IsEmpty(typeName):
               continue
            
            if not Utils.IsEmpty( att.CounterCache ):
               counterCacheColumn = att.CounterCache
            else:
               typeNameShort = self.GetType().FullName.ToLower()
               counterCacheColumn = "${typeNameShort}${DEFAULT_COUNTER_CACHE}"
            
            #debug "updateOwnerCounters() - counterCacheColumn: ${counterCacheColumn}"
            if Array.IndexOf( _activeRecordTypeColumns[typeName], counterCacheColumn) != -1:
               tblAssoc = self.GetAssociated(tableName)
               #debug "updateOwnerCounters() - working with associated ${tableName}.  record count: ${tblAssoc.Count}"
               if tblAssoc != null:
                  for arAssoc as ActiveRecordBase in tblAssoc:
                     #debug "associated ${arAssoc.GetType().FullName}: PK ${arAssoc.PrimaryKey}, ${arAssoc}"
                     updated = updateCounter(tableName, counterCacheColumn, arAssoc[arAssoc.PrimaryKey], arAssoc.PrimaryKey, count)
            #   else:
            #      debug "updateOwnerCounters() - tblAssoc was null"
            #else:
            #   debug "updateOwnerCounters() - _activeRecordTypeColumns doesn't have an entry for ${counterCacheColumn}"
      
   private def usesInheritance() as bool:
      #debug "usesInheritance() - class: ${self.GetType().FullName}, classes: ${join(_activeRecordInheritanceColumns.Keys, ',')}"
      #debug "usesInheritance() - inheritColumn: ${_activeRecordInheritanceColumns[self.GetType().AssemblyQualifiedName]}, "
      #debug "usesInheritance() - record: ${self}"
      return usesInheritance(self.GetType())
